"""Tests for osxphotos.timeutils module"""

import datetime
from zoneinfo import ZoneInfo

import pytest

from osxphotos.timeutils import (
    available_timezones,
    available_timezones_lc,
    etc_to_gmt_offset,
    get_local_utc_offset_str,
    get_valid_timezone,
    time_string_to_datetime,
    timedelta_from_gmt_str,
    timezone_for_delta_seconds,
    timezone_for_offset,
    timezone_for_timedelta,
    update_datetime,
    utc_offset_string_to_seconds,
)

# Note: these tests were generated by claude CLI and may not cover all edge cases


class TestUtcOffsetStringToSeconds:
    """Test utc_offset_string_to_seconds function"""

    def test_positive_offset_colon_format(self):
        """Test positive offset in HH:MM format"""
        assert utc_offset_string_to_seconds("+05:30") == 19800  # 5.5 hours

    def test_negative_offset_colon_format(self):
        """Test negative offset in HH:MM format"""
        assert utc_offset_string_to_seconds("-04:00") == -14400  # -4 hours

    def test_positive_offset_no_colon(self):
        """Test positive offset in HHMM format"""
        assert utc_offset_string_to_seconds("+0530") == 19800

    def test_negative_offset_no_colon(self):
        """Test negative offset in HHMM format"""
        assert utc_offset_string_to_seconds("-0400") == -14400

    def test_gmt_positive_offset(self):
        """Test GMT+HHMM format"""
        assert utc_offset_string_to_seconds("GMT+0100") == 3600

    def test_gmt_negative_offset(self):
        """Test GMT-HHMM format"""
        assert utc_offset_string_to_seconds("GMT-0500") == -18000

    def test_no_sign_defaults_positive(self):
        """Test that no sign defaults to positive"""
        assert utc_offset_string_to_seconds("05:30") == 19800

    def test_case_insensitive(self):
        """Test that input is case insensitive"""
        assert utc_offset_string_to_seconds("gmt+0100") == 3600

    def test_invalid_format_raises_error(self):
        """Test that invalid format raises ValueError"""
        with pytest.raises(ValueError, match="Invalid UTC offset format"):
            utc_offset_string_to_seconds("invalid")

    def test_single_digit_hours(self):
        """Test single digit hours"""
        assert utc_offset_string_to_seconds("+5:30") == 19800


class TestUpdateDateTime:
    """Test update_datetime function with DST-aware operations"""

    def test_naive_datetime_date_change(self):
        """Test changing date on naive datetime"""
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0)
        new_date = datetime.date(2023, 11, 20)
        result = update_datetime(dt, date=new_date)

        assert result.date() == new_date
        assert result.time() == dt.time()
        assert result.tzinfo is None  # Should remain naive

    def test_naive_datetime_time_change(self):
        """Test changing time on naive datetime"""
        # Use UTC for predictable behavior
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0, tzinfo=ZoneInfo("UTC"))
        new_time = datetime.time(15, 30, 45)
        result = update_datetime(dt, time=new_time)

        assert result.date() == dt.date()
        assert result.time() == new_time
        assert result.tzinfo == dt.tzinfo

    def test_naive_datetime_time_change_local(self):
        """Test changing time on naive datetime (local timezone)"""
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0)
        new_time = datetime.time(15, 30, 45)
        result = update_datetime(dt, time=new_time)

        assert result.date() == dt.date()
        assert result.tzinfo is None

        # Test that the function is consistent
        result2 = update_datetime(dt, time=new_time)
        assert result == result2

    def test_naive_datetime_time_delta(self):
        """Test applying time delta to naive datetime"""
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0)
        delta = datetime.timedelta(hours=2, minutes=30)
        result = update_datetime(dt, time_delta=delta)

        expected = datetime.datetime(2023, 10, 15, 14, 30, 0)
        assert result == expected
        assert result.tzinfo is None

    def test_naive_datetime_date_delta(self):
        """Test applying date delta to naive datetime"""
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0)
        delta = datetime.timedelta(days=10)
        result = update_datetime(dt, date_delta=delta)

        expected = datetime.datetime(2023, 10, 25, 12, 0, 0)
        assert result == expected
        assert result.tzinfo is None

    def test_timezone_aware_datetime_preserves_timezone(self):
        """Test that timezone-aware datetime preserves original timezone"""
        eastern = ZoneInfo("US/Eastern")
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0, tzinfo=eastern)
        delta = datetime.timedelta(hours=1)
        result = update_datetime(dt, time_delta=delta)

        assert result.tzinfo == eastern
        assert result.hour == 13

    def test_dst_transition_fall_back(self):
        """Test DST transition when clocks fall back"""
        eastern = ZoneInfo("US/Eastern")
        # November 5, 2023 - DST ends at 2 AM EST (becomes 1 AM EST)
        dt = datetime.datetime(2023, 11, 4, 1, 30, 0, tzinfo=eastern)

        # Add 25 hours - should properly handle DST transition
        result = update_datetime(dt, time_delta=datetime.timedelta(hours=25))

        # Should be November 5, 1:30 AM EST (after DST ends)
        assert result.day == 5
        assert result.hour == 1
        assert result.minute == 30

    def test_dst_transition_spring_forward(self):
        """Test DST transition when clocks spring forward"""
        eastern = ZoneInfo("US/Eastern")
        # March 12, 2023 - DST starts at 2 AM EST (becomes 3 AM EDT)
        dt = datetime.datetime(2023, 3, 11, 1, 30, 0, tzinfo=eastern)

        # Add 2 hours - should handle spring forward properly
        result = update_datetime(dt, time_delta=datetime.timedelta(hours=2))

        # Should be 3:30 AM EDT (after spring forward)
        assert result.hour == 3
        assert result.minute == 30

    def test_combined_date_time_change(self):
        """Test combining date and time changes"""
        # Use UTC for predictable behavior
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0, tzinfo=ZoneInfo("UTC"))
        new_date = datetime.date(2023, 11, 20)
        new_time = datetime.time(15, 30, 0)

        result = update_datetime(dt, date=new_date, time=new_time)

        assert result.date() == new_date
        assert result.time() == new_time
        assert result.tzinfo == dt.tzinfo

    def test_combined_date_time_change_local(self):
        """Test combining date and time changes with local timezone"""
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0)
        new_date = datetime.date(2023, 11, 20)
        new_time = datetime.time(15, 30, 0)

        result = update_datetime(dt, date=new_date, time=new_time)

        assert result.date() == new_date
        assert result.tzinfo is None

        # Test consistency
        result2 = update_datetime(dt, date=new_date, time=new_time)
        assert result == result2

    def test_local_time_delta_with_time(self):
        """Test local_time_delta is applied only when time is also provided"""
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0)
        new_time = datetime.time(15, 0, 0)
        local_delta = datetime.timedelta(hours=1)

        result_with_delta = update_datetime(
            dt, time=new_time, local_time_delta=local_delta
        )
        result_without_delta = update_datetime(dt, time=new_time)

        # The result with local_time_delta should be different from without it
        # The exact time depends on timezone conversions, but delta should be applied
        assert result_with_delta != result_without_delta
        assert result_with_delta.tzinfo is None

    def test_local_time_delta_without_time_ignored(self):
        """Test local_time_delta is ignored when time is not provided"""
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0)
        local_delta = datetime.timedelta(hours=1)

        result = update_datetime(dt, local_time_delta=local_delta)

        # Should remain unchanged since time was not provided
        assert result == dt

    def test_multiple_operations(self):
        """Test multiple operations combined"""
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0)
        new_date = datetime.date(2023, 11, 20)
        time_delta = datetime.timedelta(hours=2)

        result = update_datetime(dt, date=new_date, time_delta=time_delta)

        expected = datetime.datetime(2023, 11, 20, 14, 0, 0)
        assert result == expected

    def test_microseconds_preserved(self):
        """Test that microseconds are preserved in time operations"""
        dt = datetime.datetime(2023, 10, 15, 12, 0, 0, 123456)
        new_time = datetime.time(15, 30, 45, 654321)

        result = update_datetime(dt, time=new_time)

        assert result.microsecond == 654321


class TestTimeStringToDatetime:
    """Test time_string_to_datetime function"""

    def test_hms_format(self):
        """Test HH:MM:SS format"""
        result = time_string_to_datetime("14:30:45")
        expected = datetime.time(14, 30, 45)
        assert result == expected

    def test_hms_microseconds_format(self):
        """Test HH:MM:SS.fff format"""
        result = time_string_to_datetime("14:30:45.123456")
        expected = datetime.time(14, 30, 45, 123456)
        assert result == expected

    def test_hm_format(self):
        """Test HH:MM format"""
        result = time_string_to_datetime("14:30")
        expected = datetime.time(14, 30, 0)
        assert result == expected

    def test_invalid_format_raises_error(self):
        """Test that invalid format raises ValueError"""
        with pytest.raises(ValueError, match="Could not parse time format"):
            time_string_to_datetime("invalid_time")

    def test_invalid_time_values_raise_error(self):
        """Test that invalid time values raise ValueError"""
        with pytest.raises(ValueError):
            time_string_to_datetime("25:30:45")  # Invalid hour


class TestGetLocalUtcOffsetStr:
    """Test get_local_utc_offset_str function"""

    def test_datetime_object(self):
        """Test with datetime object"""
        dt = datetime.datetime(2023, 7, 15, 12, 0, 0)  # Summer time
        result = get_local_utc_offset_str(dt)

        # Result should be a string in ±HHMM format
        assert isinstance(result, str)
        assert len(result) == 5
        assert result[0] in ["+", "-"]
        assert result[1:3].isdigit()
        assert result[3:5].isdigit()

    def test_iso_string(self):
        """Test with ISO format string"""
        iso_string = "2023-07-15T12:00:00"
        result = get_local_utc_offset_str(iso_string)

        assert isinstance(result, str)
        assert len(result) == 5
        assert result[0] in ["+", "-"]


class TestAvailableTimezones:
    """Test timezone utility functions"""

    def test_available_timezones_returns_list(self):
        """Test that available_timezones returns a sorted list"""
        result = available_timezones()
        assert isinstance(result, list)
        assert len(result) > 0
        assert "UTC" in result
        # Check that it's sorted
        assert result == sorted(result)

    def test_available_timezones_lc_returns_lowercase(self):
        """Test that available_timezones_lc returns lowercase list"""
        result = available_timezones_lc()
        assert isinstance(result, list)
        assert len(result) > 0
        assert "utc" in result
        # Check that all are lowercase
        assert all(tz.islower() for tz in result)

    def test_cache_consistency(self):
        """Test that cached functions return consistent results"""
        result1 = available_timezones()
        result2 = available_timezones()
        assert result1 is result2  # Should be same object due to caching


class TestTimedeltaFromGmtStr:
    """Test timedelta_from_gmt_str function"""

    def test_positive_offset(self):
        """Test positive GMT offset"""
        result = timedelta_from_gmt_str("GMT+0500")
        expected = datetime.timedelta(seconds=18000)  # 5 hours
        assert result == expected

    def test_negative_offset(self):
        """Test negative GMT offset"""
        result = timedelta_from_gmt_str("GMT-0400")
        expected = datetime.timedelta(seconds=-14400)  # -4 hours
        assert result == expected

    def test_zero_offset(self):
        """Test zero offset"""
        result = timedelta_from_gmt_str("GMT+0000")
        expected = datetime.timedelta(seconds=0)
        assert result == expected

    def test_invalid_format_raises_error(self):
        """Test that invalid format raises ValueError"""
        with pytest.raises(ValueError):
            timedelta_from_gmt_str("invalid")


class TestTimezoneUtilities:
    """Test timezone lookup utility functions"""

    def test_timezone_for_offset_valid(self):
        """Test finding timezone for valid offset"""
        dt = datetime.datetime(2023, 7, 15, 12, 0, 0, tzinfo=ZoneInfo("UTC"))
        # Try to find a timezone with +05:00 offset
        try:
            result = timezone_for_offset("+05:00", dt)
            assert isinstance(result, str)
            # Verify the timezone actually has the expected offset
            tz = ZoneInfo(result)
            offset = dt.astimezone(tz).utcoffset()
            assert offset == datetime.timedelta(hours=5)
        except ValueError:
            # Some offsets might not have named timezones
            pytest.skip("No timezone found for +05:00 offset")

    def test_timezone_for_offset_invalid_raises_error(self):
        """Test that invalid offset raises ValueError"""
        dt = datetime.datetime(2023, 7, 15, 12, 0, 0, tzinfo=ZoneInfo("UTC"))
        with pytest.raises(ValueError, match="No matching named timezone found"):
            timezone_for_offset("+25:00", dt)  # Invalid offset

    def test_timezone_for_delta_seconds(self):
        """Test finding timezone for delta in seconds"""
        dt = datetime.datetime(2023, 7, 15, 12, 0, 0, tzinfo=ZoneInfo("UTC"))
        # Try UTC (0 seconds offset)
        result = timezone_for_delta_seconds(0, dt)
        assert isinstance(result, str)
        # Verify it's actually UTC
        tz = ZoneInfo(result)
        offset = dt.astimezone(tz).utcoffset()
        assert offset == datetime.timedelta(seconds=0)

    def test_timezone_for_timedelta(self):
        """Test finding timezone for timedelta"""
        dt = datetime.datetime(2023, 7, 15, 12, 0, 0, tzinfo=ZoneInfo("UTC"))
        delta = datetime.timedelta(hours=0)  # UTC offset
        result = timezone_for_timedelta(delta, dt)
        assert isinstance(result, str)

    def test_get_valid_timezone_exact_match(self):
        """Test get_valid_timezone with exact timezone name"""
        dt = datetime.datetime(2023, 7, 15, 12, 0, 0)
        result = get_valid_timezone("UTC", dt)
        assert result == "UTC"

    def test_get_valid_timezone_case_insensitive(self):
        """Test get_valid_timezone is case insensitive"""
        dt = datetime.datetime(2023, 7, 15, 12, 0, 0)
        result = get_valid_timezone("utc", dt)
        assert result.lower() == "utc"

    def test_get_valid_timezone_offset_format(self):
        """Test get_valid_timezone with offset format"""
        dt = datetime.datetime(2023, 7, 15, 12, 0, 0, tzinfo=ZoneInfo("UTC"))
        try:
            result = get_valid_timezone("GMT+0000", dt)
            assert isinstance(result, str)
        except ValueError:
            # If no matching timezone found, that's acceptable
            pass

    def test_get_valid_timezone_invalid_raises_error(self):
        """Test that invalid timezone raises ValueError"""
        dt = datetime.datetime(2023, 7, 15, 12, 0, 0)
        with pytest.raises(ValueError, match="does not appear to be a valid timezone"):
            get_valid_timezone("Invalid/Timezone", dt)


class TestDSTEdgeCases:
    """Test edge cases around DST transitions"""

    def test_naive_datetime_dst_transition(self):
        """Test naive datetime across DST transition"""
        # Test with a date that would cross DST boundary
        dt = datetime.datetime(2023, 11, 4, 23, 0, 0)  # Day before DST ends
        result = update_datetime(dt, time_delta=datetime.timedelta(hours=4))

        # Should handle DST transition properly
        assert isinstance(result, datetime.datetime)
        assert result.tzinfo is None  # Should remain naive

    def test_ambiguous_time_handling(self):
        """Test handling of ambiguous times during DST transitions"""
        eastern = ZoneInfo("US/Eastern")
        # Time that occurs twice during fall-back DST transition
        dt = datetime.datetime(2023, 11, 5, 1, 30, 0, tzinfo=eastern)

        # Should handle ambiguous time without errors
        result = update_datetime(dt, time_delta=datetime.timedelta(minutes=30))
        assert isinstance(result, datetime.datetime)
        assert result.tzinfo == eastern

    def test_non_existent_time_handling(self):
        """Test handling of non-existent times during DST transitions"""
        eastern = ZoneInfo("US/Eastern")
        # Time before spring-forward DST transition
        dt = datetime.datetime(2023, 3, 11, 1, 30, 0, tzinfo=eastern)

        # Adding time that would normally create non-existent time
        result = update_datetime(dt, time_delta=datetime.timedelta(hours=1))
        assert isinstance(result, datetime.datetime)
        assert result.tzinfo == eastern

    def test_utc_no_dst_transitions(self):
        """Test that UTC has no DST transitions"""
        utc = ZoneInfo("UTC")
        dt = datetime.datetime(2023, 3, 12, 2, 0, 0, tzinfo=utc)

        # Adding hours in UTC should be straightforward
        result = update_datetime(dt, time_delta=datetime.timedelta(hours=1))
        expected = datetime.datetime(2023, 3, 12, 3, 0, 0, tzinfo=utc)
        assert result == expected


class TestErrorConditions:
    """Test error conditions and edge cases"""

    def test_extreme_dates(self):
        """Test with extreme dates"""
        # Very old date
        dt = datetime.datetime(1900, 1, 1, 12, 0, 0)
        result = update_datetime(dt, time_delta=datetime.timedelta(hours=1))
        assert result.year == 1900
        assert result.hour == 13

    def test_leap_year_handling(self):
        """Test leap year date handling"""
        # February 29 in leap year
        dt = datetime.datetime(2020, 2, 29, 12, 0, 0)
        result = update_datetime(dt, date_delta=datetime.timedelta(days=1))
        assert result.month == 3
        assert result.day == 1

    def test_year_boundary_crossing(self):
        """Test crossing year boundaries"""
        dt = datetime.datetime(2023, 12, 31, 23, 30, 0)
        result = update_datetime(dt, time_delta=datetime.timedelta(hours=1))
        assert result.year == 2024
        assert result.month == 1
        assert result.day == 1
        assert result.hour == 0
        assert result.minute == 30


@pytest.mark.parametrize(
    "inp, expected",
    [
        ("Etc/GMT+5", "GMT-0500"),  # POSIX sign inversion
        ("Etc/GMT-3", "GMT+0300"),
        ("Etc/GMT+0", "GMT+0000"),
        ("Etc/GMT-0", "GMT+0000"),
        ("Etc/GMT+12", "GMT-1200"),  # upper typical bound
        ("Etc/GMT-14", "GMT+1400"),  # lower typical bound
    ],
)
def test_etc_to_gmt_offset_valid(inp, expected):
    assert etc_to_gmt_offset(inp) == expected


@pytest.mark.parametrize(
    "bad",
    [
        "GMT+5",  # missing Etc/ prefix
        "Etc/GMT",  # no numeric part
        "Etc/GMT+05",  # leading zero not allowed by our parser (int('05') is fine but keep as policy)
        "Etc/GMT+5:30",  # minutes not supported
        "Etc/GMT+foo",  # non-numeric
        "Etc/UTC+5",  # wrong family
    ],
)
def test_etc_to_gmt_offset_invalid(bad):
    with pytest.raises(ValueError):
        etc_to_gmt_offset(bad)
